SPB Git

spb/autoscholar

Public
2.9 KB · 65 lines typescript
1import { AppError, getInstructionVersion, uploadInstruction } from "@autoscholar/core";2import { INSTRUCTION_MAX_BYTES, UploadInstructionRequest } from "@autoscholar/schemas";3import { handler, json, parseBody } from "@/lib/server/api";4import { db } from "@/lib/server/db";5import { requireAdmin, uploadLimiter } from "@/lib/server/session";67export const runtime = "nodejs";89/** Upload (multipart `file`) or paste (JSON {content}) a research CLAUDE.md. Stores exact bytes + SHA-256 as a new immutable version. */10export const POST = handler(async (req, { params }) => {11  const sess = await requireAdmin(req, { mutation: true });12  if (!uploadLimiter.check(sess.userId).allowed) throw new AppError("rate_limited", "too many uploads", 429);13  const { id } = (await params) as { id: string };14  const ct = req.headers.get("content-type") ?? "";15  let bytes: Buffer;16  let filename = "CLAUDE.md";17  let note: string | undefined;18  if (ct.startsWith("multipart/form-data")) {19    const len = Number(req.headers.get("content-length") ?? 0);20    if (len > INSTRUCTION_MAX_BYTES * 2) throw new AppError("too_large", "upload too large", 413);21    const form = await req.formData();22    const file = form.get("file");23    if (!(file instanceof File)) throw new AppError("invalid", "multipart field `file` required", 400);24    if (file.size > INSTRUCTION_MAX_BYTES)25      throw new AppError("too_large", `file exceeds ${INSTRUCTION_MAX_BYTES} bytes`, 413);26    const allowed = ["text/markdown", "text/plain", "text/x-markdown", "application/octet-stream", ""];27    if (!allowed.includes(file.type))28      throw new AppError("invalid", `unsupported content type ${file.type}`, 400);29    bytes = Buffer.from(await file.arrayBuffer());30    filename = file.name || "CLAUDE.md";31    const n = form.get("note");32    if (typeof n === "string" && n) note = n.slice(0, 500);33  } else {34    const body = await parseBody(req, UploadInstructionRequest, INSTRUCTION_MAX_BYTES * 2);35    bytes = Buffer.from(body.content, "utf8");36    filename = body.filename;37    note = body.note;38  }39  const result = await uploadInstruction(db(), id, bytes, filename, note, { id: sess.userId, ip: sess.ip });40  const { text: _t, ...validation } = result.validation;41  return json(42    { id: result.id, version: result.version, deduplicated: result.deduplicated, validation },43    { status: result.deduplicated ? 200 : 201 },44  );45});4647export const GET = handler(async (req) => {48  await requireAdmin(req);49  const url = new URL(req.url);50  const versionId = url.searchParams.get("versionId");51  if (!versionId) throw new AppError("invalid", "versionId required", 400);52  const v = await getInstructionVersion(db(), versionId);53  return json({54    id: v.id,55    version: v.version,56    filename: v.filename,57    sha256: v.sha256,58    byteSize: v.byteSize,59    content: v.contentText,60    frontmatter: v.frontmatter,61    validation: v.validation,62    createdAt: v.createdAt,63  });64});65
Files · main
Menu

Theme